﻿using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Data;
using System.Windows.Controls.Primitives;


namespace MvvmFx.Common.ViewModels
{
    public static class ViewModelHelper
    {
        #region class variables

        public static readonly DependencyProperty ContentProperty;
        public static readonly DependencyProperty ItemsSourceProperty;
        public static readonly DependencyProperty SelectedValueProperty;
        public static readonly DependencyProperty EnableTextChangeTriggerProperty;

        #endregion


        #region constructors

        static ViewModelHelper()
        {
            // initialize dependency properties
            Type ownerType = typeof(ViewModelHelper);
            ContentProperty = DependencyProperty.RegisterAttached("Content",
                typeof(object), ownerType, new PropertyMetadata(null, 
                    RegisterContent));
            ItemsSourceProperty = DependencyProperty.RegisterAttached(
                "ItemsSource", typeof(IEnumerable), ownerType,
                new PropertyMetadata(null, RegisterItemsSource));
            SelectedValueProperty = DependencyProperty.RegisterAttached(
                "SelectedValue", typeof(object), ownerType,
                new PropertyMetadata(null, RegisterSelectedValue));
            EnableTextChangeTriggerProperty = DependencyProperty.RegisterAttached(
                "EnableTextChangeTrigger", typeof(bool), ownerType,
                new PropertyMetadata(false, RegisterTextChangeTrigger));
        }

        #endregion


        #region properties


        #region Content

        public static object GetContent(ContentControl contentControl)
        {
            return contentControl.GetValue(ContentProperty);
        }

        public static void SetContent(ContentControl contentControl,
            object content)
        {
            contentControl.SetValue(ContentProperty, content);
        }

        #endregion

        #region ItemsSource

        public static IEnumerable GetItemsSource(ItemsControl itemsControl)
        {
            return (IEnumerable)itemsControl.GetValue(ItemsSourceProperty);
        }

        public static void SetItemsSource(ItemsControl itemsControl,
            IEnumerable itemsSource)
        {
            itemsControl.SetValue(ItemsSourceProperty, itemsSource);
        }

        #endregion

        #region SelectedValue

        public static object GetSelectedValue(ItemsControl selector)
        {
            return selector.GetValue(SelectedValueProperty);
        }

        public static void SetSelectedValue(ItemsControl selector, object value)
        {
            selector.SetValue(SelectedValueProperty, value);
        }

        #endregion

        #region EnableTextChangeTrigger

        public static bool GetEnableTextChangeTrigger(TextBox textBox)
        {
            return (bool)textBox.GetValue(EnableTextChangeTriggerProperty);
        }

        public static void SetEnableTextChangeTrigger(TextBox textBox,
            bool enabled)
        {
            textBox.SetValue(EnableTextChangeTriggerProperty, enabled);
        }

        #endregion


        #endregion


        #region helper methods

        private static void RegisterContent(DependencyObject source,
            DependencyPropertyChangedEventArgs e)
        {
            // throw if source is not expected type
            ContentControl contentControl = source as ContentControl;
            if (contentControl == null)
            {
                throw new InvalidOperationException(
                    "ViewModelHelper.Content property can only be applied to ContentControl.");
            }

            // generate context for control
            object content = e.NewValue;
            new ContentControlContext(contentControl, content);
        }

        private static void RegisterItemsSource(DependencyObject source, 
            DependencyPropertyChangedEventArgs e)
        {
            // throw if source is not expected type
            ItemsControl itemsControl = source as ItemsControl;
            if (itemsControl == null)
            {
                throw new InvalidOperationException(
                    "ViewModelHelper.ItemsSource property can only be applied to ItemsControls.");
            }

            // generate context for tab control
            SelectableItemsControlContext selectableContext = null;
            IEnumerable items = e.NewValue as IEnumerable;
            if (source is TabControl)
            {
                selectableContext = new TabControlContext((TabControl)source, 
                    items);
            }

            // generate context for selector
            else if (source is Selector)
            {
                selectableContext = new SelectorContext((Selector)source, 
                    items);
            }

            // or for items control
            else
            {
                new ItemsControlContext(itemsControl, items);
            }

            // set selection (if any)
            if (selectableContext != null)
            {
                object value = GetSelectedValue(itemsControl);
                if (value != null)
                {
                    selectableContext.SetSelection(value);
                }
            }
        }

        private static void RegisterSelectedValue(
            DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            // throw if source is not expected type
            ItemsControl selector = source as ItemsControl;
            if (selector == null)
            {
                throw new InvalidOperationException(
                    "ViewModelHelper.SelectedValue property can only be applied to ItemsControl.");
            }

            // associate with context (if available)
            SelectableItemsControlContext context = Context.GetContext(selector)
                as SelectableItemsControlContext;
            if (context != null)
            {
                context.SetSelection(e.NewValue);
            }            
        }

        private static void RegisterTextChangeTrigger(DependencyObject source,
            DependencyPropertyChangedEventArgs e)
        {
            // throw if source is not expected type
            TextBox textBox = source as TextBox;
            if (textBox == null)
            {
                throw new InvalidOperationException(
                    "ViewModelHelper.EnableTextChangeTrigger property can only be applied to TextBox.");
            }

            // wire events
            bool enabled = (bool)e.NewValue;
            if (enabled == true)
            {
                textBox.TextChanged += TriggerOnTextChange;
            }

            // or unwire events
            else
            {
                textBox.TextChanged -= TriggerOnTextChange;
            }
        }

        private static void TriggerOnTextChange(object sender,
            TextChangedEventArgs e)
        {
            // resolve textbox (better be sender)
            TextBox textBox = (TextBox)sender;

            // get text binding (if any)
            BindingExpression binding = textBox.GetBindingExpression(
                TextBox.TextProperty);
            if (binding != null)
            {
                binding.UpdateSource();
            }
        }

        #endregion


        #region internal data structures

        private abstract class Context
        {
            #region class variables

            internal static readonly DependencyProperty ContextProperty;

            #endregion


            #region constructors

            static Context()
            {
                // initialize dependency properties
                Type ownerType = typeof(Context);
                ContextProperty = DependencyProperty.RegisterAttached("Context",
                    typeof(Context), ownerType, new PropertyMetadata(null));
            }

            public Context()
            {
            }

            #endregion


            #region properties


            #region Context

            public static Context GetContext(Control control)
            {
                return (Context)control.GetValue(ContextProperty);
            }

            public static void SetContext(Control control, Context context)
            {
                control.SetValue(ContextProperty, context);
            }

            #endregion


            #endregion


            #region helper methods

            protected DataTemplate FindDataTemplateForItem(
                FrameworkElement referenceElement, object item)
            {
                // skip if no item is specified
                if (item == null)
                {
                    return null;
                }

                // get item type name
                string targetType = item.GetType().FullName;

                // search visual tree resources for data template
                while (referenceElement != null)
                {
                    // search current visual's resources
                    DataTemplate dataTemplate = FindDataTemplate(targetType,
                        referenceElement.Resources);
                    if (dataTemplate != null)
                    {
                        return dataTemplate;
                    }
                         
                    // walk up to parent
                    referenceElement = VisualTreeHelper.GetParent(
                        referenceElement) as FrameworkElement;
                }

                // finally, search application resources (may return null)
                return FindDataTemplate(targetType,
                    Application.Current.Resources);
            }

            private DataTemplate FindDataTemplate(string targetType,
                ResourceDictionary resourceDictionary)
            {
                foreach (object value in resourceDictionary.Values)
                {
                    DataTemplate dataTemplate = value
                        as DataTemplate;
                    if (dataTemplate != null
                        && dataTemplate.Type == targetType)
                    {
                        return dataTemplate;
                    }
                }

                return null;
            }

            #endregion

        }  // class Context

        private class ContentControlContext : Context
        {
            #region instance variables

            private readonly ContentControl _contentControl;

            #endregion


            #region constructors

            public ContentControlContext(ContentControl contentControl,
                object content)
            {
                // initialize instance variables
                _contentControl = contentControl;

                // set initial content
                _contentControl.ContentTemplate = FindDataTemplateForItem(
                    _contentControl, content);
                _contentControl.Content = content;
            }

            #endregion

        }  // class ContentControlContext

        private class ItemsControlContext : Context
        {
            #region instance variables

            protected readonly ItemsControl _itemsControl;
            protected readonly ObservableCollection<object> _proxyCollection;
            protected readonly INotifyCollectionChanged _observableCollection;

            #endregion


            #region constructors

            public ItemsControlContext(ItemsControl itemsControl,
                IEnumerable items)
            {
                // initialize instance variables
                _itemsControl = itemsControl;
                _proxyCollection = new ObservableCollection<object>();
                _observableCollection = items as INotifyCollectionChanged;

                // ensure control is unwired
                ItemsControlContext previousContext = GetContext(itemsControl)
                    as ItemsControlContext;
                if (previousContext != null)
                {
                    // unwire event handler
                    previousContext._observableCollection.CollectionChanged -=
                        previousContext.HandleCollectionChanged;

                    // unset the value
                    itemsControl.SetValue(ContextProperty, null);
                }

                // wire control (only if using observable collection)
                if (_observableCollection != null)
                {
                    _observableCollection.CollectionChanged += 
                        HandleCollectionChanged;
                    SetContext(itemsControl, this);
                }

                // set initial items
                foreach (object item in items)
                {
                    _proxyCollection.Add(CreateContainerForItem(item));
                }
                _itemsControl.ItemsSource = _proxyCollection;
            }

            #endregion


            #region event handlers

            private void HandleCollectionChanged(object sender, 
                NotifyCollectionChangedEventArgs e)
            {
                // handle change
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        AddItems(e.NewItems, e.NewStartingIndex);
                        break;

                    case NotifyCollectionChangedAction.Replace:
                        UpdateItems(e.NewItems, e.NewStartingIndex);
                        break;

                    case NotifyCollectionChangedAction.Remove:
                        RemoveItems(e.OldItems, e.OldStartingIndex);
                        break;

                    case NotifyCollectionChangedAction.Reset:
                        RemoveAllItems();
                        break;

                    default:
                        throw new NotSupportedException();
                }
            }

            #endregion


            #region helper methods

            private void AddItems(IList items, int index)
            {
                int itemCount = items.Count;
                for (int i = 0; i < itemCount; ++i)
                {
                    object item = CreateContainerForItem(items[i]);
                    _proxyCollection.Insert(i + index, item);
                }
            }

            private void UpdateItems(IList items, int index)
            {
                int itemCount = items.Count;
                for (int i = 0; i < itemCount; ++i)
                {
                    object item = CreateContainerForItem(items[i]);
                    _proxyCollection[i + index] = item;
                }
            }

            private void RemoveItems(IList items, int index)
            {
                int itemCount = items.Count;
                for (int i = 0; i < itemCount; ++i)
                {
                    _proxyCollection.RemoveAt(index);
                }
            }

            private void RemoveAllItems()
            {
                _proxyCollection.Clear();
            }

            protected virtual object CreateContainerForItem(object item)
            {
                DataTemplate contentTemplate = FindDataTemplateForItem(
                    _itemsControl, item);
                ContentControl container = new ContentControl()
                {
                    Content = item,
                    ContentTemplate = contentTemplate
                };

                return container;
            }

            #endregion

        }  // class ItemsControlContext

        private abstract class SelectableItemsControlContext 
            : ItemsControlContext
        {
            #region constructors

            public SelectableItemsControlContext(ItemsControl selector, 
                IEnumerable items)
                : base(selector, items)
            {
            }

            #endregion


            #region abstract methods

            public abstract void SetSelection(object value);

            #endregion

        }  // class TabControlContext

        private class SelectorContext : SelectableItemsControlContext
        {
            #region constructors

            public SelectorContext(Selector selector, IEnumerable items)
                : base(selector, items)
            {
                selector.SelectionChanged += HandleSelectionChanged;
            }

            #endregion


            #region overridden methods

            public override void SetSelection(object value)
            {
                Selector selector = (Selector)_itemsControl;
                if (value == null)
                {
                    selector.SelectedIndex = -1;
                }
                else
                {
                    selector.SelectedItem = _proxyCollection.FirstOrDefault(
                        container => ((ContentControl)container).Content == value);
                }
            }

            #endregion


            #region event handlers

            private void HandleSelectionChanged(object sender,
                SelectionChangedEventArgs e)
            {
                Selector selector = (Selector)_itemsControl;
                ContentControl selection = (ContentControl)
                    selector.SelectedItem;
                SetSelectedValue(selector, selection == null ? null
                    : selection.Content);
            }

            #endregion

        }  // class TabControlContext

        private class TabControlContext : SelectableItemsControlContext
        {
            #region constructors

            public TabControlContext(TabControl tabControl, IEnumerable items)
                : base(tabControl, items)
            {
                tabControl.SelectionChanged += HandleSelectionChanged;
            }

            #endregion


            #region overridden methods

            public override void SetSelection(object value)
            {
                TabControl tabControl = (TabControl)_itemsControl;
                if (value == null)
                {
                    tabControl.SelectedIndex = -1;
                }
                else
                {
                    tabControl.SelectedItem = _proxyCollection.FirstOrDefault(
                        container => ((ContentControl)container).Content == value);
                }
            }

            protected override object CreateContainerForItem(object item)
            {
                DataTemplate contentTemplate = FindDataTemplateForItem(
                    _itemsControl, item);
                TabItem container = new TabItem()
                {
                    Header = item,
                    Content = item,
                    ContentTemplate = contentTemplate
                };

                HeaderedDataTemplate headeredDataTemplate = contentTemplate
                    as HeaderedDataTemplate;
                if (headeredDataTemplate != null)
                {
                    container.HeaderTemplate =
                        headeredDataTemplate.HeaderTemplate;
                }

                return container;
            }

            #endregion


            #region event handlers

            private void HandleSelectionChanged(object sender,
                SelectionChangedEventArgs e)
            {
                TabControl tabControl = (TabControl)_itemsControl;
                TabItem selection = (TabItem)tabControl.SelectedItem;
                SetSelectedValue(tabControl, selection == null ? null
                    : selection.Content);
            }

            #endregion

        }  // class TabControlContext

        #endregion

    }  // class ViewModelHelper

}  // namespace MvvmFx.Common.ViewModels
